我又忘記 js proxy,你可以幫我做一個教學互動的網頁嗎?
這是我丟給 AI 的第一句話。
我想要的不是一篇文字說明,而是可以按、可以看到結果的東西。
在講過程之前,先說 Proxy 是什麼。
JavaScript 的物件長這樣:
const user = { name: '小明', age: 18 }
平常我們讀取 user.name、修改 user.age = 20,都是直接對物件操作,中間沒有任何人知道。
Proxy(代理)就是在物件前面放一個「守門人」。 之後每次讀取或修改,都要先經過守門人,它可以記錄、檢查,甚至拒絕這次操作。
AI 做的網頁,最上方就是這個守門人的流程圖:
你的程式 → Proxy 守門人 → 原本的物件
畫面上有幾個按鈕:
看完這個畫面,我終於抓到重點:Proxy 不是去改物件本身,而是站在物件前面把關。
網頁下面分成好幾個步驟,每一步都會先出一道思考題,想完再按「看答案」。
第一題是:
每次有人讀取或修改
user的資料,就要印出一行紀錄。但大家還是照常寫user.name,不能改成別的寫法。你會怎麼做?
我想了一下,只想到一個比較舊的方法 Object.defineProperty,它可以替物件的「某一個屬性」加上監聽。
展開答案後,AI 剛好從這個方法講起,並指出它的問題:
name 設定完,age 還要再設定一次user.hobby = '畫畫',完全不會被記錄然後才帶出 Proxy:守門人管的是整個物件,不管是哪個屬性、什麼時候加的,全部都會經過它。
因為我先自己想過,再看到答案,比直接讀說明更容易記住。
網頁裡的範例大概是這樣:
const user = { name: '小明', age: 18 }
const p = new Proxy(user, {
get(target, key) {
console.log('有人讀取了 ' + key)
return target[key]
}
})
p.name // 印出「有人讀取了 name」,並拿到 '小明'
拆開來看:
new Proxy(物件, 規則):幫 user 建立一個守門人 p
get:「讀取」時要做的事,名字是固定的target:原本的物件,也就是 user
key:這次被讀取的屬性名稱,例如 'name'
之後只要透過 p 讀取資料,get 就會先被執行。
網頁最後有一道挑戰題:
讓陣列可以用
arr[-1]拿到最後一個元素。
一般的陣列 ['🍎', '🍌', '🍇'],arr[-1] 會拿到 undefined。挑戰是用 Proxy 讓它拿到 '🍇'。
網頁給了幾個版本讓我猜哪個是對的,每個都能直接按「執行」。其中一個版本我覺得完全沒問題:
get(target, key) {
if (key < 0) {
return target[target.length + key]
}
return target[key]
}
想法很直覺:如果 key 是負數,就用「陣列長度 + key」算出位置。長度是 3,3 + (-1) = 2,剛好是最後一個。
結果按下執行,拿到的還是 undefined。
關鍵在於:守門人收到的 key 永遠是字串,不是數字。
我們寫 arr[-1],守門人收到的其實是 '-1'(有引號的字串)。
所以這行:
target.length + key
實際上是:
3 + '-1'
在 JavaScript 裡,數字加上字串會變成「把字串接起來」,就像 1 + '1' 會得到 '11'。所以結果是 '3-1',陣列裡當然沒有叫 '3-1' 的位置。
正確的做法是先把 key 轉成數字,再來計算。
如果只是用讀的,我很可能看不出這個錯誤。親手按下執行、看到結果跟預期不一樣,才真的記住「key 是字串」。
看完網頁,我還有一個疑問:
為什麼我看到其他教學,get 和 set 裡面的參數寫法都不太一樣?
我看過有人寫 get(target, key),有人寫 get(target, prop),還有人寫 set(value) 只有一個參數,一直搞不清楚哪個才對。
AI 一樣先丟問題給我:
set(a, b, c) {
a[b] = c
return true
}
參數改成 a、b、c,還能正常運作嗎?
答案是可以。
參數的名字是我們自己取的,JavaScript 只看順序:
| 第 1 個參數 | 第 2 個參數 | 第 3 個參數 | |
|---|---|---|---|
get |
原本的物件 | 屬性名稱 | — |
set |
原本的物件 | 屬性名稱 | 要存進去的值 |
大家常寫 key 和 value,只是因為物件本來就是「key: value」的結構,讀起來比較好懂。
那 set(value) 只有一個參數又是怎麼回事?AI 告訴我,那是另一種寫法,不是 Proxy:
const user = {
_name: '小明',
get name() { return this._name },
set name(value) { this._name = value }
}
這種寫法是直接寫在物件裡,而且只管 name 這一個屬性。它已經知道自己負責的是 name,所以不需要再傳屬性名稱進來。
Proxy 就不一樣了,它管的是整個物件,任何屬性被讀取都會進到同一個 get,所以一定要告訴它「這次是哪個屬性」。
原來我一直把這兩種寫法混在一起看。
看懂不等於學會。 AI 的解說很流暢,每一句都讀得懂,很容易以為自己學會了。思考題和挑戰題一定要先自己想,不要直接看答案。
細節要回去看官方文件。 AI 的解說適合用來入門,參數順序這些細節,我還是會再對照 MDN 確認。
真正讓我記住 Proxy 的,是按下執行卻拿到 undefined 的那一刻,還有把疑問問出口的那一次。AI 做的事,是讓這些時刻更容易發生。
如果你也有學過又忘記的觀念,可以試試看:別問「X 是什麼」,改問「可以幫我做一個互動教學嗎?」